Skip to content

Wall load fixes: treatment fan-out, one CSG cut per wall, capture draw leak, converter duplicates - #762

Merged
wass08 merged 7 commits into
mainfrom
perf/wall-load-fixes
Sep 4, 2026
Merged

Wall load fixes: treatment fan-out, one CSG cut per wall, capture draw leak, converter duplicates#762
wass08 merged 7 commits into
mainfrom
perf/wall-load-fixes

Conversation

@wass08

@wass08 wass08 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Metrics (charter backlog rows 8, 9, 10)

  • IFC castle (Ifc2x3_SampleCastle.ifc, 932 walls / 464 openings / 6 levels — the production import that froze the editor): load-to-settled never (514 s) → 10.1 s, all 932 walls batched, frame 42 → 21 ms.
  • Ladder (private run-scaling-matrix.ts, vs the Wall batching stays live in cutaway #761 build, same machine): load settled −13…−29 %; wall-move settle Maxi-8× −27 %, rich-2× −23 %; undo −18…−29 % (rich 2×/4×, tower); item-move −25…−28 %; Maxi-8× end-of-run dirt 162 → 0.
  • Draw leak (charter finding 1): rich-4× draws stay 4 494 across the thumbnail timer (were 5 662 after it, permanently).

What changes (one commit each)

  1. Wall treatments — the level treatment miters were recomputed every frame while any wall on the level was dirty, and every wall renderer on the level re-rendered (65 k WallTreatments renders in 12 s on the castle; the same fan-out on every wall drag). Now: skip when the level's effective walls (identity, live overrides cached per override) and trim offsets are unchanged; per-(level, proud) miter cache; each wall subscribes to a slice holding only its own endpoint junction entries, compared structurally; no treatment work mounts for walls with no trim enabled.
  2. Openings CSG — cutouts were subtracted one by one from a growing result. Now grouped by bounding-box overlap: disjoint cutouts merge into one cutter, overlapping groups of ≤ 4 are unioned, larger groups fall back to the bounded chained path; one subtraction per wall. A wall with ≥ 6 cutouts is deferred to its own frame when the frame already rebuilt something (the budget was only checked between walls).
  3. Coincident cutouts — a real import carried the same door eight times on one wall; box cutouts contained in another box are dropped before grouping (the union chain over coincident boxes took 148 ms and froze the page).
  4. Capture draw leakthumbnail:before-capture (fired by the thumbnail autosave 30–40 s after load) made the node batch's sweep reveal every mesh held under the shared 'batched' reason, including the wall sources the merged wall batch already drew; nothing re-hid them. Wall holds now use 'wall-batched' and the wall batch handles capture itself (reveal before, re-hide after — exports still prune off-layer meshes).
  5. Placeholder sweep — a degenerate rebuilt wall (zero-length / fully cut) has as few vertices as the mount placeholder and was re-marked every 30 frames; rebuilt geometry is now stamped userData.built.
  6. IFC converter — each fill (door/window) is emitted once; spatial children use sets. Existing conversions keep their duplicates until reconverted (the editor now tolerates them).

Gates

  • Unit: viewer + nodes + ifc-converter 2 103 pass (58 new: frame skipping / override identity / level membership / selector neighbours / byte-identical trim geometry; disjoint, overlapping, coincident and nested cutouts by signed volume + material groups; budget deferral; capture holds; sweep stamp; converter dedupe on the Open House fixture).
  • E2e against the branch build: perf-regression pack 5/5, click-batched-wall, click-hidden-wall, opening-placement-preview green; placement-preview-follows-cursor fails identically on main (pre-existing).
  • Canvas parity vs the Wall batching stays live in cutaway #761 build: rich 2×/4× ≤ 0.4 % on all four poses (load / after orbit / full height / cutaway again); Maxi-4× post-orbit poses differ only by orbit end-pose drift.
  • Bakes (Maxi 1×, rich 2×): identical meshes, primitives, nodes and materials; 2 and 46 fewer triangles (one cut instead of chained cuts).

Notes

  • The castle stays a top-of-ladder fixture in the private repo (project_IfcCastle2x3Smpl); its remaining cost is the steady-state frame (21 ms at 5 600 draws), not load.
  • Found on the way, not addressed here: select mode mounts three transparent corner-bracket meshes per ceiling corner for every ceiling on the level (1 236 meshes on rich-4×) — charter row 11.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN


Note

Medium Risk
Touches core wall CSG, treatment subscriptions, and scene-layer holds; behavior is heavily tested but geometry and render paths are easy to regress subtly.

Overview
Targets wall load and steady-state cost after large IFC imports: fewer redundant treatment updates, cheaper opening booleans, no permanent double-draw after thumbnails, and unique doors/windows from conversion.

Wall treatments skip level miter rebuilds when effective walls (with cached live overrides) and trim proud keys are unchanged; a per-level miter cache and per-wall zustand selectors (endpoint junction slices only) stop every wall on a level from re-rendering when one wall is dirty. Trim mounts only when hasWallTreatments is true.

Wall geometry groups opening cutouts by overlap, drops contained box duplicates, unions small groups (≤4) via mergeWallCutoutBrushes, then does one CSG subtraction per wall (with chained fallback for large groups). Progressive rebuilds use shouldDeferWallRebuild so walls with ≥6 cutouts wait for a fresh frame. Rebuilt meshes get userData.built so degenerate walls are not swept as placeholders.

Batching / visibility uses a distinct 'wall-batched' hold reason; WallBatchSystem reveals source meshes on thumbnail:before-capture and re-hides after capture so exports see geometry without leaving walls drawn twice.

IFC converter dedupes spatial children with Sets and emits each door/window fill once (expressIdToNodeId guard + register after successful node creation), with fixture tests for repeated relationships.

Reviewed by Cursor Bugbot for commit 5e2243b. Bugbot is set up for automated code reviews on this repo. Configure here.

wass08 and others added 6 commits September 4, 2026 15:30
…, subscribe per wall

While any wall on a level was dirty, the treatment system recomputed the
level's miters every frame (uncached, once per proud offset) and wrote a
fresh object to its store; every wall renderer on the level subscribed to
that object, so all of them re-rendered each frame — 65k WallTreatments
renders in 12 s on the IFC castle, and the same fan-out on every wall drag.

Now the system skips a level whose effective walls (identity, with live
overrides cached per override) and proud offsets are unchanged, caches the
per-proud miter data on those inputs, and each wall subscribes to a slice
holding only its own endpoint intersections, compared structurally. Walls
with no trim enabled mount no treatment work at all.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN
…their own frame

Each opening was subtracted in turn from a result that grew with every
cut, so a wall with 20 openings paid 20 passes over an ever-larger mesh
(70 ms in one call on the IFC castle). The cutouts are now grouped by
bounding-box overlap: disjoint ones are merged as-is, overlapping groups go
through a real union, and the wall is cut once. The progressive rebuild
loop also defers a wall with six or more cutouts when the frame has already
rebuilt something, instead of checking the time budget only between walls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN
The project thumbnail autosave fires 30–40 s after load and emits
thumbnail:before-capture. The node batch answers by revealing every mesh
held under the shared 'batched' reason — including the wall sources the
merged wall batch was already drawing — and nothing re-hid them: every
batched wall drew twice (and cast shadows twice) for the rest of the
session. Measured on rich-4×: 4 494 → 5 662 draws, 11.8 → 17.1 ms, forever.

Wall holds now carry their own reason, and the wall batch handles capture
itself: sources come back for the capture (exports still prune off-layer
meshes) and go under again on thumbnail:after-capture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN
A real IFC import carries the same door eight times on one wall; coincident
boxes are the worst input a boolean can get, and a union chain over them
took 148 ms and then froze the page. Box cutouts fully inside another box
are dropped before grouping, overlapping groups of up to four are unioned,
and larger groups fall back to the bounded chained subtraction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN
Every void relationship for a wall appended to its opening list, and the
emission loop created a fresh node per visit, so a fill referenced by
repeated relationships became eight identical doors on one wall (the
sample castle). A fill belongs to one opening, which voids one host, so
emitted fill ids are now tracked globally; spatial children use sets.
Existing converted scenes keep their duplicates until reconverted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN
…ay built

A zero-length or fully cut wall rebuilds to as few vertices as the mount
placeholder, and the sweep re-marked it every 30 frames — 21 castle walls
rebuilt forever and their level's batch re-sewed with them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN
@pascal

pascal Bot commented Sep 4, 2026

Copy link
Copy Markdown

I hit an error while handling your request (Model unavailable on AI Gateway free tier: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted…).

Please try again, rephrase, or reach out if it keeps failing.

Error id: 5e32fcc2-5966-4b9c-a3ab-43f3e2cc60ee

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN
@wass08
wass08 merged commit c405122 into main Sep 4, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant